/**
 * @name app_scene.js
 * @fileOverview This file has functions related to documenting the Mojo App Scene
 * framework abstraction that tracks and manages app scene visibility state
 * transitions.

Copyright 2009 Palm, Inc.  All rights reserved.

*/

/**
 * @namespace
 * An app is a collection of primitive abstractions for representing the
 * structure of applications.
 */
App = {};

/**
 * @Class
 * @description Base class for tracking and managing app scene states from the
 * framework / system's perspective.  Example of such states include:
 *   activated/deactivated, shown/hidden, maximized/minimized.
 *
 * To use this class in your application, follow these steps:
 *
 * 1. Have your class inherit from App.Scene like so:
 *    @example
 *    MySceneAssistant = Class.create(App.Scene, {
 *         ... // your methods.
 *    });
 *
 * 2. Invoke the super constructor from your constructor like so:
 *    @example
 *        initialize: function($super, <your args>) {
 *            $super();  // Invoke super constructor.
 *            ... // initialize your stuff here.
 *        }
 *
 *    You should always call the super constructor before doing your own
 *    app specific initialization.
 *
 * 3. Implement the following methods in your class if they are applicable to
 *    you. These methods are all optional depending on what work your app needs
 *    to do.  If the app does not implement them, then the framework will not
 *    try to call them.
 *
 *    a. setupSubscriptions()
 *       - set up your subscriptions here.
 *
 *         This method will be called by the framework immediately after the
 *         app scene's setup method is called.  This gives the app a chance to
 *         do some initialization work before its subscriptions are setup.
 *
 *    b. cleanupSubscriptions()
 *       - cancel your subscriptions here.
 *
 *         This method will be called by the framework immediately before the
 *         app scene's cleanup method is called.  This ensures that subscriptions
 *         are cleaned up before the app shuts down other things.  Last
 *         initialized, and first cleaned up.
 *
 *    c. enableSubscriptions()
 *       - enable the use of your subscriptions here.  In the existing service
 *         infrastructure, this simply means setting up the subscriptions if
 *         they aren't already set up by setupSubscriptions.
 *
 *         This method will be called whenever the framework decides that your
 *         scene has become active and needs data from its subscriptions.
 *         An example of this is when your scene is now shown on screen after
 *         either being hidden or obscured completely previously. 
 *
 *    d. disableSubscriptions()
 *       - disable the use of your subscriptions here.  In the existing service
 *         infrastructure, this could mean cancelling subscriptions that you
 *         have set up in enableSubscriptions() above.
 *
 *         This method will be called whenever the framework decides that your
 *         scene no longer needs data from it subscriptions.  An example of
 *         this is when your scene transitions to a hidden or completely
 *         obscured state.
 *
 *    e. maximizeSubscriptions()
 *       - maximize the rate of your subscriptions here i.e. handle
 *         subscription events at the fastest reasonable speed.
 *
 *         If the backend service provides a maximize function, then you
 *         use that to "maximize" the data throughput within reason.
 *
 *         This method will be called whenever the framework decides that your
 *         scene will be occupying the full screen.
 *
 *    f. minimizeSubscriptions()
 *       - minimize the rate of your subscriptions here i.e. reduce the
 *         rate of subscription event handling to a slower more reasonable
 *         rate given that such events will not be commanding the full
 *         attention of the user at this time.
 *
 *         If the backend service provides a minimize function, then you
 *         use that to "minimize" the data throughput to a lower reasonable
 *         within reason.
 *
 *       Scene Visibility State change events:
 *       ====================================
 *    g. activate()
 *       - indicates that your scene is now active.
 *         This is the same callback function that the existing framework
 *         already calls to activate your scene.
 *
 *    h. deactivate()
 *       - indicates that your scene is now deactivate.
 *         This is the same callback function that the existing framework
 *         already calls to deactivate your scene.
 *
 *    i. show()
 *       - indicates that your scene is now visible.
 *
 *    j. hide()
 *       - indicates that your scene is now hidden or completely obscured
 *         by another scene.
 *
 *    k. maximize()
 *       - indicates that your scene is now taking up the full screen.
 *
 *    l. minimize()
 *       - indicates that your scene is now reduced to card size.
 *
 *    NOTE: a scene may be active and maximized/minimized, shown/hidden.
 *          a scene may be deactivate and maximized/minimized, shown/hidden.
 *          a scene may only be maximized if it is shown.
 *          a scene may be minimized when active/deactivated, shown/hidden.
 *          a scene may be shown when activated/deactivated, maximized/minimized.
 *          a scene may be hidden when activated/deactivated, maximized/shown.
 */
App.Scene = Class.create({
	/** @public
	 * @description Constructor/initializer.
	*/
	initialize: function(delaySetup) {
		//console.log("*************************** App.Scene.initialize()");
		this.delaySetup = delaySetup;

		// Bypass app specific methods with our Scene methods:
		this.appSetup = this.setup;
		this.setup = this._setup;
		this.appCleanup = this.cleanup;
		this.cleanup = this._cleanup;
		if (this.aboutToActivate) {
			this.appAboutToActivate = this.aboutToActivate;
		}
		this.aboutToActivate = this._aboutToActivate;
		this.appActivate = this.activate;
		this.activate = this._activate;
		this.appDeactivate = this.deactivate;
		this.deactivate = this._deactivate;

		// Requested scene state and device state:
		this.shouldBeActivated = true;
		this.shouldBeVisible = true;
		this.shouldBeMaximized = true;
		this.hasDisplayOn = true;
		this.hasLockOn = false;

		// Actual scene state:
		this.isActivated = false;
		this.isVisible = false;
		this.isMaximized = false;

		this.handleDisplayStatusFailureCount = 0;
		this.handleLockStatusFailureCount = 0;

		// Subscriptions:
		this.displayStatus = undefined;
		this.lockStatus = undefined;

		// Handlers:
		this.maximizeHandler = this.notifyMaximize.bind(this);
		this.minimizeHandler = this.notifyMinimize.bind(this);
		this.displayStatusCallbackHandler = this.handleDisplayStatus.bind(this);
		this.displayStatusFailedCallbackHandler = this.handleDisplayStatusFailed.bind(this);
		this.lockStatusCallbackHandler = this.handleLockStatus.bind(this);
		this.lockStatusFailedCallbackHandler = this.handleLockStatusFailed.bind(this);
	},

	/** @private */
	_setup: function() {
		//console.log("*************************** App.Scene.setup()");
		if (this.appSetup) this.appSetup();

		// If requested, delay as much the setup to later since another scene
		// is being immediately pushed on top of this one.
	  	if (this.delaySetup === true) {
			this.setupTimer = this._setupDelayed.bind(this).delay(4);
		} else {
			this._setupDelayed();
		}
	},
	
	_setupDelayed: function() {
		this.setupTimer = undefined;
		if (this.setupDelayed) this.setupDelayed();
		// Handlers:
		if (this.maximizeSubscriptions || this.maximize) {
			Mojo.Event.listen(this.controller.stageController.document,
							  Mojo.Event.stageActivate, this.maximizeHandler, true);
		}
		if (this.minimizeSubscriptions || this.minimize) {        
			Mojo.Event.listen(this.controller.stageController.document,
							  Mojo.Event.stageDeactivate, this.minimizeHandler, true);
		}

		this.displayStatus =
			new Mojo.Service.Request('palm://com.palm.display/control', {
				method: 'status',
				parameters: {'subscribe': true},
				onSuccess: this.displayStatusCallbackHandler,
				onFailure: this.displayStatusFailedCallbackHandler
			});

		this.lockStatus =
			new Mojo.Service.Request('palm://com.palm.systemmanager/', {
				method: 'getLockStatus',
				parameters: {'subscribe': true},
				onSuccess: this.lockStatusCallbackHandler,
				onFailure: this.lockStatusFailedCallbackHandler
			});

		// We default to activated, shown, and maximized states initially:
		this.isActivated = true;
		this.isVisible = true;
		this.isMaximized = true;
		this.hasSubscriptionsEnabled = false;

		if (this.setupSubscriptions) {
			this.setupSubscriptions();
			this.hasSubscriptionsEnabled = true;
		}
	},

	/** @private */
	_cleanup: function() {
		//console.log("*************************** App.Scene.cleanup()");
		if (this.displayStatus) this.displayStatus.cancel();
		if (this.lockStatus) this.lockStatus.cancel();

		if (this.cleanupSubscriptions) this.cleanupSubscriptions();

		Mojo.Event.stopListening(this.controller.stageController.document,
								 Mojo.Event.stageActivate, this.maximizeHandler, true);
		Mojo.Event.stopListening(this.controller.stageController.document,
								 Mojo.Event.stageDeactivate, this.minimizeHandler, true);
		if (this.appCleanup) this.appCleanup();
	},

	/** @private */
	_aboutToActivate: function(callback) {
		//console.log("*************************** App.Scene.aboutToActivate()");
		this.isActivated = true;
		this.notifyShow(); // Simulate show event for now.

		if (this.appAboutToActivate) {
			this.appAboutToActivate(callback);
		} else {
			callback();
		}
	},

	/** @private */
	_activate: function() {
		//console.log("*************************** App.Scene.activate()");
		if (this.appActivate) {
			this.appActivate();
		}
	},

	/** @private */
	_deactivate: function() {
		//console.log("*************************** App.Scene.deactivate()");
		this.notifyHide(); // Simulate hide event for now.
		if (this.appDeactivate) {
			this.appDeactivate();
		}
		this.isActivated = false;
	},

	/** @private */
	notifyShow: function() {
		this.shouldBeVisible = true;
		this.handleShow();
	},
	/** @private */
	notifyHide: function() {
		this.shouldBeVisible = false;
		this.handleHide();
	},
	/** @private */
	notifyMaximize: function() {
		this.shouldBeMaximized = true;
		this.handleMaximize();
	},
	/** @private */
	notifyMinimize: function() {
		this.shouldBeMaximized = false;
		this.handleMinimize();
	},

	/** @private */
	handleShow: function() {
		if (!this.isVisible && (this.shouldBeVisible && this.hasDisplayOn && !this.hasLockOn)) {
			//console.log("*************************** App.Scene.handleShow()");
			
			// In case _setupDelayed hasn't yet been called, do it now.
			if (this.setupTimer !== undefined) {
				Mojo.Log.info("App.Scene.handleShow calling _setupDelayed")
				clearTimeout(this.setupTimer);
				this._setupDelayed();
			}

			this.handleMaximize();
			if (this.show) this.show();
			if (this.enableSubscriptions && !this.hasSubscriptionsEnabled) {
				this.enableSubscriptions();
				this.hasSubscriptionsEnabled = true;
			}
			this.isVisible = true;
		}
	},
	/** @private */
	handleHide: function() {
		if (this.isVisible && (!this.shouldBeVisible || !this.hasDisplayOn || this.hasLockOn)) {
			//console.log("*************************** App.Scene.handleHide()");
			// Technically, we can stop listening for maximize/minimize events here too.
			// But we're simulating show() with maximize().  So, we'll need keep these
			// alive.

			if (this.hide) this.hide();
			if (this.disableSubscriptions && this.hasSubscriptionsEnabled) {
				this.disableSubscriptions();
				this.hasSubscriptionsEnabled = false;
			}
			this.isVisible = false;
			this.handleMinimize();
		}
	},
	/** @private */
	handleMaximize: function() {
		if (!this.isMaximized && (this.shouldBeMaximized && this.shouldBeVisible
								  && this.hasDisplayOn && !this.hasLockOn)) {

			//console.log("*************************** App.Scene.handleMaximize()");
			if (this.maximize) this.maximize();
			if (this.maximizeSubscriptions && this.hasSubscriptionsEnabled) {
				this.maximizeSubscriptions();
			}
			this.isMaximized = true;
		}
	},
	/** @private */
	handleMinimize: function() {
		if (this.isMaximized && (!this.shouldBeMaximized || !this.shouldBeVisible
								 || !this.hasDisplayOn || this.hasLockOn)) {

			//console.log("*************************** App.Scene.handleMinimize()");
			if (this.minimize) this.minimize();
			if (this.minimizeSubscriptions && this.hasSubscriptionsEnabled) {
				this.minimizeSubscriptions();
			}
			this.isMaximized = false;
		}
	},
	/** @private */
	handleDisplayStatus: function(event) {
		//console.log("*************************** App.Scene.handleDisplayStatus()");
		if (event.event == "displayOn") {
			this.hasDisplayOn = true;
			this.handleShow();
		} else if (event.event == "displayOff") {
			this.hasDisplayOn = false;
			this.handleHide();
		} else if (event.event == "displayDimmed") {
			// Do nothing for now.

		// Initial response to subscription:
		} else if (event.event == "request") {
			this.handleDisplayStatusFailureCount = 0;
			if (event.state == "on") {
				this.hasDisplayOn = true;
				this.handleShow();
			} else if (event.state == "off") {
				this.hasDisplayOn = false;
				this.handleHide();
			} else if (event.state == "dimmed") {
				// Do nothing for now.
			}
		}
	},
	/** @private */
	handleDisplayStatusFailed: function(err) {
		//console.log("*************************** App.Scene.handleDisplayStatusFailed()");
		this.handleDisplayStatusFailureCount++;
		if (this.handleDisplayStatusFailureCount < 3) {
			Mojo.Log.error("handleDisplayStatusFailureCount=", this.handleDisplayStatusFailureCount,
						   " detail=", $H(err).inspect());
			if (this.displayStatus) {
				this.displayStatus.cancel();
			}
			this.displayStatus =
				new Mojo.Service.Request('palm://com.palm.display/control', {
					method: 'status',
					parameters: {'subscribe': true},
					onSuccess: this.displayStatusCallbackHandler,
			    	onFailure: this.displayStatusFailedCallbackHandler
				});
		} else {
			Mojo.Log.error("FATAL: cannot subscribe to display status: " + $H(err).inspect());
		}
	},
	/** @private */
	handleLockStatus: function(event) {
		//console.log("*************************** App.Scene.handleLockStatus()");
		this.handleLockStatusFailureCount = 0;
		if (event.locked == true) {
			this.hasLockOn = true;
			this.handleHide();
		} else if (event.locked == false) {
			this.hasLockOn = false;
			this.handleShow();
		}
	},
	/** @private */
	handleLockStatusFailed: function(err) {
		//console.log("*************************** App.Scene.handleLockStatusFailed()");
		this.handleLockStatusFailureCount++;
		if (this.handleLockStatusFailureCount < 3) {
			Mojo.Log.error("handleLockStatusFailureCount=", this.handleLockStatusFailureCount,
						   " detail=", $H(err).inspect());
			if (this.lockStatus) {
				this.lockStatus.cancel();
			}
			this.lockStatus =
				new Mojo.Service.Request('palm://com.palm.systemmanager/', {
					method: 'getLockStatus',
					parameters: {'subscribe': true},
					onSuccess: this.lockStatusCallbackHandler,
			    	onFailure: this.lockStatusFailedCallbackHandler
				});
		} else {
			Mojo.Log.error("FATAL: cannot subscribe to lock status: " + $H(err).inspect());
		}
	},
});
